﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace Pavilion
{
    public delegate void LineExecutionHandler(Vector2 previousPoint, Vector2 currentPoint);
    public class PolygonBox
    {
        public event Action<PolygonBox> PolygonBoxChanged;
        public event Action<Vector2, int> PointAdded;

        public int Count { get { return pointCollection.Count; } }
        public Vector2 this[int index] { get { return pointCollection[index]; } set { SetPoint(index, value); } }

        public Rectangle Rectangle { get { return rectangle; } }
        public IEnumerable<Vector2> PositionedPointCollection { get { return positionedPointCollection; } }
        public Vector2 Origin { get { return origin; } set { origin = value; actualPosition = position - origin;  UpdatePositionPointCollection(); } }
        public Vector2 Position { get { return position; } set { position = value; actualPosition = position - origin; UpdatePositionPointCollection(); } }
        public bool IsHorizontallyFlipped { get { return isHorizontallyFlipped; } set { isHorizontallyFlipped = value; UpdatePositionPointCollection(); } }

        private bool isMultipleChanges, isHorizontallyFlipped;
        private List<Vector2> pointCollection, positionedPointCollection, edges;
        private Rectangle rectangle;
        private Vector2 position, origin, actualPosition;

        public PolygonBox()
        {
            this.rectangle = new Rectangle();
            this.pointCollection = new List<Vector2>();
            this.positionedPointCollection = new List<Vector2>();
        }

        public PolygonBox(params Vector2[] points) : this()
        {
            this.pointCollection.AddRange(points);

            foreach (Vector2 point in points)
                this.positionedPointCollection.Add(CaclulatePositionOfPoint(point));

            DetermineRectangleBounds();
        }

        public void ExecuteOperationPerLine(LineExecutionHandler lineExecutionHandler)
        {
            if (Count > 1)
            {
                Vector2 previousPoint = PositionedPointCollection.Last();

                foreach (Vector2 point in PositionedPointCollection)
                {
                    lineExecutionHandler.Invoke(previousPoint, point);
                    previousPoint = point;
                }
            }
        }

        public void Add(Vector2 point)
        {
            pointCollection.Add(point);
            positionedPointCollection.Add(CaclulatePositionOfPoint(point));

            DetermineRectangleBounds();
            OnPointAdded(point, pointCollection.Count - 1);
        }

        public void AddRange(params Vector2[] points)
        {
            int prevousCount = this.pointCollection.Count;
            int addedPointsCount = points.Count();

            pointCollection.AddRange(points);

            foreach (Vector2 point in points)
                positionedPointCollection.Add(CaclulatePositionOfPoint(point));

            DetermineRectangleBounds();

            for (int i = 0; i < addedPointsCount; i++)
                OnPointAdded(this.pointCollection[prevousCount + i], prevousCount + i);
        }

        public bool Remove(Vector2 point)
        {
            int index = pointCollection.IndexOf(point);

            if (index == -1)
                return false;

            RemoveAt(index);

            return true;
        }

        public void RemoveAt(int index)
        {
            pointCollection.RemoveAt(index);
            positionedPointCollection.RemoveAt(index);
            DetermineRectangleBounds();
        }

        public void Clear()
        {
            pointCollection.Clear();
            positionedPointCollection.Clear();
            DetermineRectangleBounds();
        }

        public bool Intersects(PolygonBox polygonBox)
        {
            if (!Intersects(this.Rectangle, polygonBox.Rectangle))
                return false;

            for (int i = 0; i < this.positionedPointCollection.Count; i++)
            {
                if (IsInsidePolygon(polygonBox, this.positionedPointCollection[i].X, this.positionedPointCollection[i].Y))
                    return true;
            }

            for (int i = 0; i < polygonBox.positionedPointCollection.Count; i++)
            {
                if (IsInsidePolygon(this, polygonBox.positionedPointCollection[i].X, polygonBox.positionedPointCollection[i].Y))
                    return true;
            }

            if (polygonBox.Count > 1 && this.Count > 1)
            {
                Vector2 previousPoint = polygonBox.PositionedPointCollection.Last();

                foreach (Vector2 point in polygonBox.PositionedPointCollection)
                {
                    if (LineVector.Intersects(this, new LineVector(previousPoint, point - previousPoint)))
                        return true;

                    previousPoint = point;
                }
            }

            return false;
        }

        public Vector2 PredictCollisionDepthAgainst(LineVector lineVector, Vector2 moveAmount)
        {
            return PredictCollisionDepthAgainst(new Vector2[] { lineVector.StartPosition, lineVector.EndPosition }, moveAmount);
        }

        public Vector2 PredictCollisionDepthAgainst(IEnumerable<Vector2> pointCollection, Vector2 moveAmount)
        {
            List<Vector2> predictedPoints = new List<Vector2>();

            foreach (Vector2 point in PositionedPointCollection)
                predictedPoints.Add(point + moveAmount);

            return CalculateCollisionDepthAgainst(predictedPoints, pointCollection);
        }

        public Vector2 CalculateCollisionDepthAgainst(LineVector lineVector)
        {
            return CalculateCollisionDepthAgainst(PositionedPointCollection, new Vector2[] { lineVector.StartPosition, lineVector.EndPosition });
        }

        public Vector2 CalculateCollisionDepthAgainst(PolygonBox polygonBox)
        {
            return CalculateCollisionDepthAgainst(PositionedPointCollection, polygonBox.PositionedPointCollection);
        }

        public Vector2 CalculateCollisionDepthAgainst(IEnumerable<Vector2> pointCollectionA, IEnumerable<Vector2> pointCollectionB)
        {
            float minIntervalDistance = float.PositiveInfinity;
            Vector2 translationAxis = new Vector2();

            if (!CalculateMinDistanceOfEdge(pointCollectionA, pointCollectionB, ref minIntervalDistance, ref translationAxis))
                return default(Vector2);

            if (!CalculateMinDistanceOfEdge(pointCollectionB, pointCollectionA, ref minIntervalDistance, ref translationAxis))
                return default(Vector2);

            Vector2 center = CalculateCenter(pointCollectionA) - CalculateCenter(pointCollectionB);

            if (Vector2.Dot(center, translationAxis) < 0)
                translationAxis = -translationAxis;

            return translationAxis * minIntervalDistance;
        }

        private bool CalculateMinDistanceOfEdge(IEnumerable<Vector2> pointCollectionA, IEnumerable<Vector2> pointCollectionB, ref float minIntervalDistance, ref Vector2 translationAxis)
        {
            if (pointCollectionA.Count() > 0)
            {
                Vector2 previousPoint = pointCollectionA.Last();

                foreach (Vector2 currentPoint in pointCollectionA)
                {
                    if (!CalculateMinDistanceOfEdge(pointCollectionA, pointCollectionB, currentPoint - previousPoint, ref minIntervalDistance, ref translationAxis))
                        return false;
                    previousPoint = currentPoint;
                }
            }

            return true;
        }

        private bool CalculateMinDistanceOfEdge(IEnumerable<Vector2> pointCollectionA, IEnumerable<Vector2> pointCollectionB, Vector2 edge, ref float minIntervalDistance, ref Vector2 translationAxis)
        {
            // Find the axis perpendicular to the current edge
            Vector2 axis = new Vector2(-edge.Y, edge.X);
            axis.Normalize();

            float intervalDistance = IntervalDistance(pointCollectionA, pointCollectionB, axis);

            // Check if the polygon projections are currentlty intersecting
            if (intervalDistance > 0)
                return false;

            // Check if the current interval distance is the minimum one. If so store
            // the interval distance and the current distance.
            // This will be used to calculate the minimum translation vector
            intervalDistance = Math.Abs(intervalDistance);
            if (intervalDistance < minIntervalDistance)
            {
                minIntervalDistance = intervalDistance;
                translationAxis = axis;
            }

            return true;
        }

        private Vector2 CalculateCenter(IEnumerable<Vector2> points)
        {
            Vector2 total = default(Vector2);

            foreach (Vector2 point in points)
                total += point;

            return (total / (float)points.Count());
        }

        // Calculate the distance between [minA, maxA] and [minB, maxB]
        // The distance will be negative if the intervals overlap
        private float IntervalDistance(IEnumerable<Vector2> polygonPointListA, IEnumerable<Vector2> polygonPointListB, Vector2 axis)
        {
            float minA = 0, maxA = 0, minB = 0, maxB = 0;

            ProjectPolygon(axis, polygonPointListA, ref minA, ref maxA);
            ProjectPolygon(axis, polygonPointListB, ref minB, ref maxB);

            if (minA < minB)
                return minB - maxA;
            else
                return minA - maxB;
        }

        // Calculate the projection of a polygon on an axis and returns it as a [min, max] interval
        private void ProjectPolygon(Vector2 axis, IEnumerable<Vector2> polygonPoints, ref float min, ref float max)
        {
            // To project a point on an axis use the dot product
            float d = Vector2.Dot(axis, polygonPoints.First());
            min = d;
            max = d;

            foreach (Vector2 point in polygonPoints)
            {
                d = Vector2.Dot(point, axis);

                if (d < min)
                    min = d;
                else if (d > max)
                    max = d;
            }
        }

        public void PerformMultipleChanges(Action<PolygonBox> multipleChangesAction)
        {
            isMultipleChanges = true;

            multipleChangesAction.Invoke(this);

            isMultipleChanges = false;
            DetermineRectangleBounds();
        }

        public override string ToString()
        {
            if (pointCollection.Count == 0)
                return "Empty PolygonBox";

            string toString = "";

            foreach (Vector2 point in pointCollection)
                toString += "(" + point + ") ";

            return toString;
        }

        private void DetermineRectangleBounds()
        {
            if (isMultipleChanges)
                return;

            if (positionedPointCollection.Count == 0)
            {
                rectangle = new Rectangle();
                return;
            }

            float left = positionedPointCollection[0].X;
            float right = positionedPointCollection[0].X;
            float top = positionedPointCollection[0].Y;
            float bottom = positionedPointCollection[0].Y;

            for (int i = 1; i < positionedPointCollection.Count; i++)
            {
                if (positionedPointCollection[i].X < left)
                    left = positionedPointCollection[i].X;
                else if (positionedPointCollection[i].X > right)
                    right = positionedPointCollection[i].X;

                if (positionedPointCollection[i].Y < top)
                    top = positionedPointCollection[i].Y;
                else if (positionedPointCollection[i].Y > bottom)
                    bottom = positionedPointCollection[i].Y;
            }

            rectangle.X = (int)left;
            rectangle.Y = (int)top;
            rectangle.Width = (int)(right - left);
            rectangle.Height = (int)(bottom - top);

            OnPolygonBoxChanged();
        }

        private void UpdatePositionPointCollection()
        {
            for (int i = 0; i < positionedPointCollection.Count; i++)
            {
                Vector2 position = CaclulatePositionOfPoint(pointCollection[i]);

                if (position.X == 20)
                    throw new Exception();

                positionedPointCollection[i] = position;
            }

            OnPolygonBoxChanged();
        }

        private Vector2 CaclulatePositionOfPoint(Vector2 point)
        {
            if (IsHorizontallyFlipped)
                point.X = origin.X + origin.X - point.X;

            return point + actualPosition;
        }

        private void SetPoint(int index, Vector2 value)
        {
            pointCollection[index] = value;
            positionedPointCollection[index] = value + position - origin;
            DetermineRectangleBounds();
        }

        private void OnPolygonBoxChanged()
        {
            if (PolygonBoxChanged != null)
                PolygonBoxChanged(this);
        }

        private void OnPointAdded(Vector2 point, int index)
        {
            if (PointAdded != null)
                PointAdded(point, index);
        }

        public static PolygonBox CreateCirclePolygon(float radius, float sidesCount)
        {
            float max = 2 * (float)Math.PI;
            float step = max / sidesCount;
            PolygonBox polygonBox = new PolygonBox();

            polygonBox.PerformMultipleChanges((box) =>
            {
                for (float theta = 0; theta < max; theta += step)
                {
                    box.Add(new Vector2(radius * (float)Math.Cos((double)theta),
                        radius * (float)Math.Sin((double)theta)));
                }
            });

            return polygonBox;
        }

        public static bool IsInsideRectangle(Rectangle rectangle, Vector2 position)
        {
            return IsInsideRectangle(rectangle, position.X, position.Y);
        }

        public static bool IsInsideRectangle(Rectangle rectangle, float x, float y)
        {
            return x >= rectangle.Left && x <= rectangle.Right && y >= rectangle.Top && y <= rectangle.Bottom;
        }

        public static bool IsInsidePolygon(PolygonBox polygonBox, float x, float y)
        {
            bool oddNodes = false;

            //This is a more efficient way, but less accurate since it doesn't cover all polygons. But it covers most of them, especially the more common ones.
            //if (polygonBox.Count > 2)
            //{
            //    Vector2 previousPoint = polygonBox.PositionedPointCollection.Last();

            //    foreach (Vector2 point in polygonBox.PositionedPointCollection)
            //    {
            //        if ((point.Y < y && previousPoint.Y >= y || previousPoint.Y < y && point.Y >= y) && (point.X <= x || previousPoint.X <= x))
            //        {
            //            if (point.X + (y - point.Y) / (previousPoint.Y - point.Y) * (previousPoint.X - point.X) < x)
            //                oddNodes = !oddNodes;
            //        }

            //        previousPoint = point;
            //    }
            //}

            if (polygonBox.Count > 2)
            {
                Vector2 previousPoint = polygonBox.PositionedPointCollection.Last();

                foreach (Vector2 point in polygonBox.PositionedPointCollection)
                {
                    if (point.Y < y && previousPoint.Y >= y || previousPoint.Y < y && point.Y >= y)
                    {
                        if (point.X + (y - point.Y) / (previousPoint.Y - point.Y) * (previousPoint.X - point.X) < x)
                            oddNodes = !oddNodes;
                    }

                    previousPoint = point;
                }
            }

            return oddNodes;
        }

        public static bool Intersects(Rectangle a, Rectangle b)
        {
            if (a.Right > b.Left && a.Left < b.Right && a.Top < b.Bottom && a.Bottom > b.Top)
                return true;

            return false;
        }
    }
}
